package cn.good.yan.service.t1.myLock;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 自定义的Redis锁，实现Lock接口
 *
 * @author shengren.yan
 * @create 2023-12-04
 */
public class RedisDistributedLock implements Lock {

    @Resource
    private RedisTemplate redisTemplate;
    private String lockName;    // KEYS[1] 分布式的key
    private String uuidValue;   // ARGV[1]  uuid+线程id
    private long expireTime;    // ARGV[2]  超时时间 默认30秒

    public RedisDistributedLock(RedisTemplate redisTemplate, String lockName, String uuidValue) {
        this.redisTemplate = redisTemplate;
        this.lockName = lockName;
        this.uuidValue = uuidValue + ":" + Thread.currentThread().getId();
        this.expireTime = 30L;
    }

    // 加锁
    @Override
    public void lock() {
        tryLock();
    }

    // 加锁
    @Override
    public boolean tryLock() {
        try {
            tryLock(-1L, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    // 加锁 - 此操作是 Lua 脚本，具有原子性 or  也执行可重入锁
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if (time == -1L) {
            String script = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
                    "redis.call('hincrby',KEYS[1],ARGV[1],1)  " +
                    "redis.call('expire',KEYS[1],ARGV[2]) " +
                    "return 1 " +
                    "else " +
                    "return 0 " +
                    "end";
            System.out.println("lockName = " + lockName +"\t" + "uuidValue = " + uuidValue);
            Boolean execute = (Boolean) redisTemplate.execute(new DefaultRedisScript(script, Boolean.class),
                    Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
            while (!execute) {
                // 暂停 60ms  60毫秒
                TimeUnit.MICROSECONDS.sleep(60);
            }
            // 新建一个后台扫描程序，来监视key目前的ttl，是否到我们规定的 1/2 1/3 来实现续期
            // 看门狗机制，监听如果该程序逻辑没有执行完，自动续期
            resetExpire();
            return true;
        }
        return false;
    }

    // 解锁  此操作是 Lua 脚本，具有原子性 or 也执行可重入锁
    @Override
    public void unlock() {
        String script = "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
                "return nil " +
                "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then  " +
                "return redis.call('del',KEYS[1]) " +
                "else  " +
                "return 0 " +
                "end";
        // nil = false  1 = true  0 = false，有3个返回值就需要Long
        Long flag = (Long) redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
        if (null == flag) {
            throw new RuntimeException("this lock doesn't exists0");
        }
    }

    // 看门狗机制
    private void resetExpire() {
        String script = "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
                "return redis.call('expire',KEYS[1],ARGV[2]) " +
                "else " +
                "return 0 " +
                "end";
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                Boolean execute = (Boolean) redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
                if (execute) {
                    resetExpire();
                }
            }
        }, (this.expireTime * 1000) / 3);
    }


    // 锁中断的场景
    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    // 给该锁配置钥匙
    @Override
    public Condition newCondition() {
        return null;
    }

}
